package com.eyalbira.loadingdots;

import com.eyalbira.loadingdots.utils.LogUtil;
import com.eyalbira.loadingdots.utils.TypedAttrUtils;
import com.eyalbira.loadingdots.utils.ValueAnimator;

import ohos.agp.animation.Animator;
import ohos.agp.animation.AnimatorValue;
import ohos.agp.colors.RgbColor;
import ohos.agp.components.AttrSet;
import ohos.agp.components.Component;
import ohos.agp.utils.LayoutAlignment;
import ohos.app.Context;

import ohos.global.resource.NotExistException;
import ohos.global.resource.WrongTypeException;
import ohos.agp.utils.Color;

import ohos.agp.components.element.ShapeElement;
import ohos.agp.components.Image;
import ohos.agp.components.DirectionalLayout;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 *
 * Customizable bouncing dots view for smooth loading effect.
 * Mostly used in chat bubbles to indicate the other person is typing.
 *
 * GitHub: https://github.com/EyalBira/loading-dots
 * Created by eyalbiran on 12/5/15.
 */
public class LoadingDots extends DirectionalLayout implements Component.EstimateSizeListener,
        Component.BindStateChangedListener {
    public static final int DEFAULT_DOTS_COUNT = 3;
    public static final int DEFAULT_LOOP_DURATION = 600;
    public static final int DEFAULT_LOOP_START_DELAY = 100;
    public static final int DEFAULT_JUMP_DURATION = 400;

    private List<Component> mDots;
    private ValueAnimator mAnimation;
    private boolean mIsAttachedToWindow;

    private boolean mAutoPlay;

    // Dots appearance attributes
    private int mDotsColor;
    private int mDotsCount;
    private int mDotSize;
    private int mDotSpace;

    // Animation time attributes
    private int mLoopDuration;
    private int mLoopStartDelay;

    // Animation behavior attributes
    private int mJumpDuration;
    private int mJumpHeight;

    // Cached Calculations
    private int mJumpHalfTime;
    private int[] mDotsStartTime;
    private int[] mDotsJumpUpEndTime;
    private int[] mDotsJumpDownEndTime;

    public LoadingDots(Context context) throws NotExistException, WrongTypeException, IOException {
        super(context);
        init(null);
    }

    public LoadingDots(Context context, AttrSet attrs) throws NotExistException, WrongTypeException, IOException {
        super(context, attrs);
        init(attrs);
    }

    public LoadingDots(Context context, AttrSet attrs, String defStyleAttr)
            throws NotExistException, WrongTypeException, IOException {
        super(context, attrs, defStyleAttr);
        init(attrs);
    }

    private void init(AttrSet attrs) throws NotExistException, WrongTypeException, IOException {
        Context context = getContext();
        if (attrs != null) {
            mAutoPlay = TypedAttrUtils.getBoolean(attrs, "LoadingDots_auto_play", true);
            mDotsColor = TypedAttrUtils.getColor(attrs, "LoadingDots_dots_color", Color.GRAY);
            mDotsCount = TypedAttrUtils.getInteger(attrs, "LoadingDots_dots_count", DEFAULT_DOTS_COUNT);
            mDotSize = TypedAttrUtils.getDimensionPixelSize(attrs, "LoadingDots_dots_size",
                    context.getResourceManager().getElement(ResourceTable.Integer_LoadingDots_dots_size_default).getInteger());
            mDotSpace = TypedAttrUtils.getDimensionPixelSize(attrs, "LoadingDots_dots_space",
                    context.getResourceManager().getElement(ResourceTable.Integer_LoadingDots_dots_space_default).getInteger());
            mLoopDuration = TypedAttrUtils.getInteger(attrs, "LoadingDots_loop_duration", DEFAULT_LOOP_DURATION);
            mLoopStartDelay = TypedAttrUtils.getInteger(attrs, "LoadingDots_loop_start_delay", DEFAULT_LOOP_START_DELAY);
            mJumpDuration = TypedAttrUtils.getInteger(attrs, "LoadingDots_jump_duration", DEFAULT_JUMP_DURATION);
            mJumpHeight = TypedAttrUtils.getDimensionPixelSize(attrs, "LoadingDots_jump_height",
                    context.getResourceManager().getElement(ResourceTable.Integer_LoadingDots_jump_height_default).getInteger());
        } else {
            mAutoPlay = true;
            mDotsColor = Color.GRAY.getValue();
            mDotsCount = DEFAULT_DOTS_COUNT;
            mDotSize = context.getResourceManager().getElement(ResourceTable.Integer_LoadingDots_dots_size_default).getInteger();
            mDotSpace = context.getResourceManager().getElement(ResourceTable.Integer_LoadingDots_dots_space_default).getInteger();
            mLoopDuration = DEFAULT_LOOP_DURATION;
            mLoopStartDelay = DEFAULT_LOOP_START_DELAY;
            mJumpDuration = DEFAULT_JUMP_DURATION;
            mJumpHeight = context.getResourceManager().getElement(ResourceTable.Integer_LoadingDots_jump_height_default).getInteger();
        }
        // Setup LinerLayout
        setOrientation(HORIZONTAL);
        setAlignment(LayoutAlignment.BOTTOM);

        setEstimateSizeListener(this);

        calculateCachedValues();
        initializeDots(context);
        setBindStateChangedListener(this);
    }


    @Override
    public boolean onEstimateSize(int widthMeasureSpec, int heightMeasureSpec) {
        super.setEstimatedSize(widthMeasureSpec, heightMeasureSpec);
        // We allow the height to save space for the jump height
        setEstimatedSize(getEstimatedWidth(), getEstimatedHeight() + mJumpHeight);
        return false;
    }


    @Override
    public void onComponentBoundToWindow(Component component) {
        mIsAttachedToWindow = true;

        createAnimationIfAutoPlay();
        if (mAnimation != null && getVisibility() == VISIBLE) {
            mAnimation.start();
        }
    }

    @Override
    public void onComponentUnboundFromWindow(Component component) {
        mIsAttachedToWindow = false;
        if (mAnimation != null) {
            mAnimation.end();
        }
    }

    @Override
    public void setVisibility(int visibility) {
        super.setVisibility(visibility);
        switch (visibility) {
            case VISIBLE:
                createAnimationIfAutoPlay();
                startAnimationIfAttached();
                break;
            case INVISIBLE:
            case HIDE:
                if (mAnimation != null) {
                    mAnimation.end();
                }
                break;
        }
    }

    private Component createDotView(Context context) {
        Image dot = new Image(context);
        ShapeElement shapeElement = new ShapeElement();
        shapeElement.setShape(ShapeElement.OVAL);
        dot.setImageElement(shapeElement);
        shapeElement.setRgbColor(RgbColor.fromArgbInt(mDotsColor));
        return dot;
    }

    private void startAnimationIfAttached() {
        if (mIsAttachedToWindow && !mAnimation.isRunning()) {
            mAnimation.start();
        }
    }

    private void createAnimationIfAutoPlay() {
        if (mAutoPlay) {
            createAnimation();
        }
    }

    private void createAnimation() {
        if (mAnimation != null) {
            // We already have an animation
            return;
        }
        calculateCachedValues();
        initializeDots(getContext());

        mAnimation = ValueAnimator.ofInt(0, mLoopDuration);
        mAnimation.setValueUpdateListener(new AnimatorValue.ValueUpdateListener() {
            @Override
            public void onUpdate(AnimatorValue animatorValue, float value) {

                int dotsCount = mDots.size();
                int from = 0;

                int animationValue = (int) value;

                if (animationValue < mLoopStartDelay) {
                    // Do nothing
                    return;
                }

                for (int i = 0; i < dotsCount; i++) {
                    Component dot = mDots.get(i);

                    int dotStartTime = mDotsStartTime[i];

                    float animationFactor;
                    if (animationValue < dotStartTime) {
                        // No animation is needed for this dot yet
                        animationFactor = 0f;
                    } else if (animationValue < mDotsJumpUpEndTime[i]) {
                        // Animate jump up
                        animationFactor = (float) (animationValue - dotStartTime) / mJumpHalfTime;
                    } else if (animationValue < mDotsJumpDownEndTime[i]) {
                        // Animate jump down
                        animationFactor = 1 - ((float) (animationValue - dotStartTime - mJumpHalfTime) / (mJumpHalfTime));
                    } else {
                        // Dot finished animation for this loop
                        animationFactor = 0f;
                    }

                    float translationY = (-mJumpHeight - from) * animationFactor;
                    dot.setTranslationY(translationY);
                }
            }
        });
        mAnimation.setDuration(mLoopDuration);
        mAnimation.setLoopedCount(Animator.INFINITE);
    }

    public final void startAnimation() {
        if (mAnimation != null && mAnimation.isRunning()) {
            // We are already running
            return;
        }

        createAnimation();
        startAnimationIfAttached();
    }

    public final void stopAnimation() {
        if (mAnimation != null) {
            mAnimation.end();
            mAnimation = null;
        }
    }

    private void calculateCachedValues() {
        verifyNotRunning();

        // The offset is the time delay between dots start animation
        int startOffset = (mLoopDuration - (mJumpDuration + mLoopStartDelay)) / (mDotsCount - 1);

        // Dot jump half time ( jumpTime/2 == going up == going down)
        mJumpHalfTime = mJumpDuration / 2;

        mDotsStartTime = new int[mDotsCount];
        mDotsJumpUpEndTime = new int[mDotsCount];
        mDotsJumpDownEndTime = new int[mDotsCount];

        for (int i = 0; i < mDotsCount; i++) {
            int startTime = mLoopStartDelay + startOffset * i;
            mDotsStartTime[i] = startTime;
            mDotsJumpUpEndTime[i] = startTime + mJumpHalfTime;
            mDotsJumpDownEndTime[i] = startTime + mJumpDuration;
        }
    }

    private void verifyNotRunning() {
        if (mAnimation != null) {
            throw new IllegalStateException("Can't change properties while animation is running!");
        }
    }

    private void initializeDots(Context context) {
        verifyNotRunning();
        removeAllComponents();

        // Create the dots
        mDots = new ArrayList<>(mDotsCount);
        LayoutConfig dotParams = new LayoutConfig(mDotSize, mDotSize);
        LayoutConfig spaceParams = new LayoutConfig(mDotSpace, mDotSize);
        for (int i = 0; i < mDotsCount; i++) {
            // Add dot
            Component dotView = createDotView(context);
            addComponent(dotView, dotParams);
            mDots.add(dotView);

            // Add space
            if (i < mDotsCount - 1) {
                addComponent(new Component(context), spaceParams);
            }
        }
    }

    // Setters and getters

    /**
     * Set AutoPlay to true to play the loading animation automatically when view is attached and visible.
     * xml: LoadingDots_auto_play
     * @param autoPlay
     */
    public void setAutoPlay(boolean autoPlay) {
        mAutoPlay = autoPlay;
    }

    public boolean getAutoPlay() {
        return mAutoPlay;
    }

    /**
     * Set the color to be used for the dots fill color
     * xml: LoadingDots_dots_color
     * @param color resolved color value
     */
    public void setDotsColor(int color) {
        verifyNotRunning();
        mDotsColor = color;
    }

    /**
     * Set the color to be used for the dots fill color
     * xml: LoadingDots_dots_color
     * @param colorRes color resource
     */
    public void setDotsColorRes(int colorRes) {
        try {
            setDotsColor(getContext().getResourceManager().getElement(colorRes).getColor());
        } catch (IOException | NotExistException | WrongTypeException exception) {
            LogUtil.error("LoadingDots", exception.getMessage());
        }
    }

    public int getDotsColor() {
        return mDotsColor;
    }

    /**
     * Set the number of dots
     * xml: LoadingDots_dots_count
     * @param count dots count
     */
    public void setDotsCount(int count) {
        verifyNotRunning();
        mDotsCount = count;
    }

    public int getDotsCount() {
        return mDotsCount;
    }

    /**
     * Set the dots size
     * xml: LoadingDots_dots_size
     * @param size size in pixels
     */
    public void setDotsSize(int size) {
        verifyNotRunning();
        mDotSize = size;
    }

    /**
     * Set the dots size
     * xml: LoadingDots_dots_size
     * @param sizeRes size resource
     */
    public void setDotsSizeRes(int sizeRes) {
        try {
            setDotsSize(getContext().getResourceManager().getElement(sizeRes).getInteger());
        } catch (IOException | NotExistException | WrongTypeException exception) {
            LogUtil.error("LoadingDots", exception.getMessage());
        }
    }

    public int getDotsSize() {
        return mDotSize;
    }

    /**
     * Set the space between dots
     * xml: LoadingDots_dots_space
     * @param space space in pixels
     */
    public void setDotsSpace(int space) {
        verifyNotRunning();
        mDotSpace = space;
    }

    /**
     * Set the space between dots
     * xml: LoadingDots_dots_space
     * @param spaceRes space size resource
     */
    public void setDotsSpaceRes(int spaceRes) {
        try {
            setDotsSpace(getContext().getResourceManager().getElement(spaceRes).getInteger());
        } catch (IOException | NotExistException | WrongTypeException exception) {
            LogUtil.error("LoadingDots", exception.getMessage());
        }
    }

    public int getDotsSpace() {
        return mDotSpace;
    }

    /**
     * Set the loop duration. This is the duration for the entire animation loop (including start delay)
     * xml: LoadingDots_loop_duration
     * @param duration duration in milliseconds
     */
    public void setLoopDuration(int duration) {
        verifyNotRunning();
        mLoopDuration = duration;
    }

    public int getLoopDuration() {
        return mLoopDuration;
    }

    /**
     * Set the loop start delay. Each loop will delay the animation by the given value.
     * xml: LoadingDots_loop_start_delay
     * @param startDelay delay duration in milliseconds
     */
    public void  setLoopStartDelay(int startDelay) {
        verifyNotRunning();
        mLoopStartDelay = startDelay;
    }

    public int getLoopStartDelay() {
        return mLoopStartDelay;
    }

    /**
     * Set the dots jump duration. This is the duration it takes a single dot to complete the jump.
     * Jump duration starts when the dot first start to rise until it settle back to base location.
     * xml: LoadingDots_jump_duration
     * @param jumpDuration jumpDuration of the dot
     */
    public void setJumpDuraiton(int jumpDuration) {
        verifyNotRunning();
        mJumpDuration = jumpDuration;
    }

    public int getJumpDuration() {
        return mJumpDuration;
    }

    /**
     * Set the jump height of the dots. The entire view will include this height to allow the dots
     * animation to draw properly. The entire view height will be DotsSize + JumpHeight.
     * xml: LoadingDots_jump_height
     * @param height size in pixels
     */
    public void setJumpHeight(int height) {
        verifyNotRunning();
        mJumpHeight = height;
    }

    /**
     * Set the jump height of the dots. The entire view will include this height to allow the dots
     * animation to draw properly. The entire view height will be DotsSize + JumpHeight.
     * xml: LoadingDots_jump_height
     * @param heightRes size resource
     */
    public void setJumpHeightRes(int heightRes) {
        try {
            setJumpHeight(getContext().getResourceManager().getElement(heightRes).getInteger());
        } catch (IOException | NotExistException | WrongTypeException exception) {
            LogUtil.error("LoadingDots", exception.getMessage());
        }
    }

    public int getJumpHeight() {
        return mJumpHeight;
    }
}

